package de.jmda.core.util;

import java.io.File;
import java.util.Arrays;
import java.util.HashSet;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.log4j.Logger;

/**
 *
 */
public abstract class RuntimeUtil
{
	public final static String FILE_SEPARATOR =
			System.getProperty("file.separator");
	
	public final static String PATH_SEPARATOR =
			System.getProperty("path.separator");
	
	public final static String JAVA_CLASS_PATH =
			System.getProperty("java.class.path");

	private final static Logger LOGGER = Logger.getLogger(RuntimeUtil.class);

	private final static File CLASS_PROPERTIES_FILE =
			new File(RuntimeUtil.class.getName() + ".properties");

	private static final HashSet<String> FILTER_STACKTRACE_TOKENS =
			new HashSet<String>();

	private static final String FILTER_STACKTRACE_PROPERTY_KEY     =
			"FILTER_STACKTRACE_PREFIX_KEY";
	private static final String FILTER_STACKTRACE_PROPERTY_DEFAULT =
			"org.eclipse.jdt.internal.junit.runner.RemoteTestRunner;" +
			"junit.framework.TestSuite;" +
			"junit.framework.TestCase;" +
			"junit.framework.TestResult;" +
			"java.lang.reflect.Method;" +
			"sun.reflect.DelegatingMethodAccessorImpl;" +
			"sun.reflect.NativeMethodAccessorImpl;" +
			"sun.reflect.NativeConstructorAccessorImpl;" +
			"access$1";
	private static final String FILTER_STACKTRACE_PROPERTY;

	static
	{
		// initialise constants from properties stored in CLASS_PROPERTIES_FILE
		PropertiesConfiguration configuration;

		try
		{
			configuration = new PropertiesConfiguration(CLASS_PROPERTIES_FILE);
		}
		catch (ConfigurationException e)
		{
			throw new ExceptionInInitializerError(
					"failure loading property configuration\n" + e);
		}
		
		String filterStacktrace = configuration.getString(FILTER_STACKTRACE_PROPERTY_KEY);
		
		if (filterStacktrace == null)
		{
			FILTER_STACKTRACE_PROPERTY = FILTER_STACKTRACE_PROPERTY_DEFAULT;
			
			configuration.setProperty(FILTER_STACKTRACE_PROPERTY_KEY, FILTER_STACKTRACE_PROPERTY);

			try
			{
				configuration.save(CLASS_PROPERTIES_FILE);
			}
			catch (ConfigurationException e)
			{
				LOGGER.warn("failure writing class properties file", e);
			}
		}
		else
		{
			FILTER_STACKTRACE_PROPERTY = filterStacktrace;
		}
		
		FILTER_STACKTRACE_TOKENS.addAll(Arrays.asList(FILTER_STACKTRACE_PROPERTY.split(";")));
	}

	public static final Class<?> getThisClass()
	{
		Class<?>          result = null;
		StackTraceElement ste    =
				RuntimeUtil.getThisStackTraceElement(new Throwable());

		try
		{
			result = Class.forName(ste.getClassName());
		}
		catch (ClassNotFoundException e)
		{
			System.err.println("failure getting class for " + ste.getClassName());
		}

		return result;
	}

	public static final String getThisMethodName()
	{
		StackTraceElement ste =
				RuntimeUtil.getThisStackTraceElement(new Throwable());

		return ste.getMethodName();
	}

	public static final StackTraceElement getThisStackTraceElement()
	{
		return RuntimeUtil.getThisStackTraceElement(new Throwable());
	}

	/**
	 * ###pending: documentation###
	 *
	 * @return ###pending: documentation###
	 */
	public static final Class<?> getCallingClass()
	{
		Class<?> result = null;

		StackTraceElement ste =
				RuntimeUtil.getCallingStackTraceElement(new Throwable());

		if (ste != null)
		{
			try
			{
				result = Class.forName(ste.getClassName());
			}
			catch (ClassNotFoundException e)
			{
				System.err.println("failure getting class for " + ste.getClassName());
			}
		}
		else
		{
			// this happens when this method is called from main methods
		}

		return result;
	}

	/**
	 * ###pending: documentation###
	 *
	 * @return ###pending: documentation###
	 */
	public static final String getCallingMethodName()
	{
		String result = null;
		
		StackTraceElement ste =
				RuntimeUtil.getCallingStackTraceElement(new Throwable());

		if (ste != null)
		{
			result = ste.getMethodName();
		}
		else
		{
			// this happens when this method is called from main methods
		}

		return result;
	}

	/**
	 * ###pending: documentation###
	 *
	 * @param t ###pending: documentation###
	 *
	 * @return ###pending: documentation###
	 */
	public static final String callstackToString(Throwable t)
	{
		StringBuffer result = new StringBuffer();
		
		StackTraceElement[] stes = t.getStackTrace();

		for (int i = 0; i < stes.length; i++)
		{
			result.append("[" + i + "] class [" + stes[i].getClassName() +
										"] method [" + stes[i].getMethodName() + "]\n");
		}

		return result.toString();
	}

	/**
	 * ###pending: documentation###
	 *
	 * @param t ###pending: documentation###
	 * @param idx ###pending: documentation###
	 *
	 * @return ###pending: documentation###
	 */
	public static final Class<?> getClassFromCallstackElement(
			Throwable t, int idx)
	{
		if (t == null)
		{
			throw new IllegalArgumentException("Throwable must not be null");
		}

		if (idx < 0)
		{
			throw new IllegalArgumentException("idx must not be less than zero");
		}
		
		StackTraceElement[] stes = t.getStackTrace();
		
		if (idx >= stes.length)
		{
			throw new IllegalArgumentException("idx must not be equal or greater than " +
																				 stes.length);
		}
		
		Class<?> result = null;
		
		try
		{
			result = Class.forName(stes[idx].getClassName());
		}
		catch (ClassNotFoundException e)
		{
			throw new RuntimeException("can't find class for name " +
																 stes[idx].getClassName());
		}
		
		return result;
	}

	private static final StackTraceElement getThisStackTraceElement(
			Throwable t)
	{
		StackTraceElement result = null;
		StackTraceElement[] stes = t.getStackTrace();

		// iterate stacktrace, skip first element
		for (int i = 1; i < stes.length; i++)
		{
			// check if className must be filtered
			if (RuntimeUtil.filteredElement(stes[i]))
			{
				// ignore and continue
			}
			else
			{
				// found a stack trace element that does not have to be filtered, it is
				// "this" stack trace element
				return stes[i];
			}
		}

		LOGGER.error(
				"failed finding this stack trace element for\n" +
				callstackToString(t));

		return result;
	}

	private static final StackTraceElement getCallingStackTraceElement(
			Throwable t)
	{
		StackTraceElement[] stackTraceElements = t.getStackTrace();
		
		boolean skippedFirstUnfiltered = false;

//System.out.println("callstack\n" + RuntimeUtil.callstackToString(t));

		// iterate stacktrace, skip first element
		for (int i = 1; i < stackTraceElements.length; i++)
		{
			// check if className must be filtered
			if (RuntimeUtil.filteredElement(stackTraceElements[i]))
			{
				// ignore and continue
			}
			else
			{
				// found a stack trace element that does not have to be filtered
				if (skippedFirstUnfiltered == false)
				{
					// it is "this" stack trace element so we have to skip it
					skippedFirstUnfiltered = true;
				}
				else
				{
					return stackTraceElements[i];
				}
			}
		}
		
		LOGGER.error(
				"failed finding calling stack trace element for\n" +
				callstackToString(t));

		return null;
	}
	
	private static boolean filteredElement(StackTraceElement stackTraceElement)
	{
		return
				FILTER_STACKTRACE_TOKENS.contains(stackTraceElement.getClassName()) ||
				FILTER_STACKTRACE_TOKENS.contains(stackTraceElement.getMethodName());
	}

	private static void runSelfTest()
	{
		System.out.println("run:  getThisClass         [" + RuntimeUtil.getThisClass()         + "]");
		System.out.println("run:  getThisMethodName    [" + RuntimeUtil.getThisMethodName()    + "]");
		System.out.println("run:  getCallingClass      [" + RuntimeUtil.getCallingClass()      + "]");
		System.out.println("run:  getCallingMethodName [" + RuntimeUtil.getCallingMethodName() + "]");
	}
	
	/**
	 * ###pending: documentation###
	 *
	 * @param args ###pending: documentation###
	 */
	public static void main(String[] args)
	{
		System.out.println("main: getThisClass         [" + RuntimeUtil.getThisClass()         + "]");
		System.out.println("main: getThisMethodName    [" + RuntimeUtil.getThisMethodName()    + "]");
		System.out.println("main: getCallingClass      [" + RuntimeUtil.getCallingClass()      + "]");
		System.out.println("main: getCallingMethodName [" + RuntimeUtil.getCallingMethodName() + "]");
		
		RuntimeUtil.runSelfTest();
	}
}